/****************************************************************************
 *
 *  LinuxChatty
 *  Copyright (C) 2010, David Hsu.
 * 
 *  This program is free software; you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation; either version 2 of the License, or
 *  (at your option) any later version.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License along
 *  with this program; if not, write to the Free Software Foundation, Inc.,
 *  51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 ***************************************************************************/
#include "ThreadService.h"
#include "HTTPClient.h"
#include "xml/tinyxml.h"
#include "html.h"
#include "b64.h"
#include <string>
#include <vector>
#include <map>
#include <stdio.h>

// Determines what format to download from the API
// TODO: This should be a constant stored in some resource class
#define THREAD_FORMAT "xml"

using namespace std;

/**
 * Destructor to clear away all the memory
 */
ThreadService::~ThreadService()
{
	ClearAllCommentData();
}

void ThreadService::Reset()
{
	isLoadingStory = false;
	ClearAllCommentData();
}

void ThreadService::SetStoryId(unsigned int id)
{
    storyId = id;
}

unsigned int ThreadService::GetStoryId() const
{
    return storyId;
}

void ThreadService::SetThreadId(unsigned int id)
{
    threadId = id;
}

unsigned int ThreadService::GetThreadId() const
{
    return threadId;
}

void ThreadService::SetCurrentCommentIndex(unsigned int index)
{
    currentCommentIndex = index;
}

/**
 * Set the API server to use.
 * @param url Server URL to use.
 */
void ThreadService::SetAPIServer(const string& url)
{
	client.SetHost(url);
}

/**
 * Refreshes the current story id and updates the comment structure.
 * @param pageNumber Page in the story to refresh. Refreshes the first page if
 *                   id is 0.
 * @preturn True if the thread was refreshed correctly. False otherwise.
 */
bool ThreadService::GetCurrentStory(unsigned int pageNumber)
{
    return GetStory( storyId, pageNumber );
}

/**
 * Refreshes the specified story id and updates the comment structure with the
 * information.
 * @param id ID of the story to retrieve. Refreshes the latest chatty if the id
 *        is 0.
 * @param pageNumber Page in the story to refresh. Refreshes the first page if 
 *        pageNumber is 0.
 * @return True if the thread was refreshed correctly. False if it failed.
 */
bool ThreadService::GetStory(unsigned int id, unsigned int pageNumber)
{
	ClearAllCommentData();
	
	// Get data from the chatty server for the story we want
	// If the id < 0, then it defaults to refreshing the first page of the 
	// latest
	int error;
	if (id == 0)
	{
		error = client.CommDownload("/", "");
	}
	else
	{
		// TODO! Wow, such a conversion mess.  Maybe I should do sprintf for the
		// whole thing
		// Retrieves the specified story and page
		// Converts the digits to a string so we can send it
		char storyId[16], numString[16];
		sprintf(storyId, "%d", id);
		sprintf(numString, "%d", pageNumber);
		string strStoryId(storyId);
		string strPage(numString);
		string path("/" + strStoryId + "." + strPage + "." + THREAD_FORMAT);
		error = client.CommDownload(path, "");
	}
	
	if (error < 0)
	{
		// We have a problem trying to get to the server
		return false;
	}
		
	TiXmlDocument stream;
	const char * pReturn = stream.Parse(client.GetBody().c_str());
	// Parse returns null on success
	if (pReturn == NULL)
	{
		isLoadingStory = true;
		return ParseXML(stream);
	}
	return false;
}

/**
 * Get the current thread tree. This returns all comment data for that thread.
 * @return True if the thread was refreshed correctly. False otherwise.
 */
bool ThreadService::GetCurrentThread()
{
    return GetThread( threadId );
}

/**
 * Get the thread tree of the specified id. This returns all the comment data
 * for that thread.
 * @param id ID of the story to retrieve. Refreshes the latest chatty if the
 *        id is 0.
 * @return True if the thread was refreshed correctly. False if it failed.
 */
bool ThreadService::GetThread(unsigned int id)
{
	ClearCommentData();
	
	// Get data from the chatty server for the story we want
	// Retrieves the specified thread
	// Converts the digits to a string so we can send it
	char postId[16];
	sprintf(postId, "%d", id);
	string strId(postId);
	string path("/thread/" + strId + "." + THREAD_FORMAT);
	int error = client.CommDownload(path, "");

	if (error < 0)
	{
		// We have a problem trying to get to the server
		return false;
	}
	
	TiXmlDocument stream;
	const char * pReturn = stream.Parse(client.GetBody().c_str());
	// Parse returns null on success
	if (pReturn == NULL)
	{
		isLoadingStory = false;
		return ParseXML(stream);
	}
	return false;
}

/**
 * Posts the text for a new comment or reply
 * @param threadId Post that this is replying to.  If postId is 0, then create a
 *        new root thread
 * @param text Actual body text that will be posted to the thread
 * @param error Return string of any error that it may have encountered.
 * @return True if the post succeeded.  False if it failed.
 */
bool ThreadService::PostData(unsigned int threadId, const string& text,
    string& error)
{
	// Converts the digits to a string so we can send it
	bool status = false;
	char chThreadId[16];
	sprintf(chThreadId, "%d", threadId);
	string strThreadId(chThreadId);
	error = "";
	
	// Create the POST variables for sending the comment
	string postData = "&body=" + text;
	postData += "&parent_id=" + strThreadId;
	
	if (client.CommDownload("/post/", postData, authorizationString) == 0)
	{
		// Check the response and see what the result was
		if (client.GetResponse() ==  "200" || client.GetResponse() == "201")
		{
		    status = true;
		    
		    // Check the body of the text to see if there were any
		    // errors with the posting.
		    string body = client.GetBody();
		    if (body == "error_login_failed")
		    {
		        error = "Login failed! Check authorization.";
		    }
		    else if (body == "error_post_body_missing")
		    {
		        error = "No post body. Enter text.";
		    }
		    else if (body == "error_account_banned")
		    {
		        error = "Banned from posting.";
		    }
		    else if (body == "error_post_rate_limiter")
		    {
		        error = "Post rate limit reached. Please wait a few minutes.";
		    }
		    else if (body == "error_post_at_least_5_characters")
		    {
		        error = "Please have at least 5 characters.";
		    }
		    else if (strncmp(body.c_str(), "error_", 6) == 0)
		    {
		        error = "Unexpected problem encountered.";
		    }
		    
		    if (error != "")
		    {
		        status = false;
		    }
	    }
	}
	return status;
}

/**
 * Sets the authorization string from a file.
 * @param authorization Base64 string that's the username:password authorization
 */
void ThreadService::SetAuthorization(const std::string& authorization)
{
	authorizationString = authorization;
}

/**
 * Accessor function for getting the authorization string.
 */
const string& ThreadService::GetAuthorization() const
{
	return authorizationString;
}

/**
 * Creates the authorization string needed to send with a post.
 * @param username User that is posting.
 * @param password Password for the poster.
 * @return Bas64 authorization string to send.
 */
const string ThreadService::EncodeAuthorization(const string& username,
    const string& password)
{
	string userInfo = username + ":" + password;
	
	// Base64 encoding
	char auth[64];
	memset(auth, 0, 64);
	if (b64encode(userInfo.length(), (char *)userInfo.c_str(), 64, auth) != 0)
	{
		// Base64 really shouldn't fail, but if for some reason it does, the
		// authorization will fail as well
		// TODO: Error handling?
		return "";
	}
	string strAuth = auth;
	return strAuth;
}

/**
 * Returns the username and password of a Base64 authorization string.
 * @param authorization Base64 encoding of the username:password
 * @param username Username string that will be returned
 * @param password Password string that will be returned
 * @return True if the authorization was successfully decoded.  False otherwise.
 */
bool ThreadService::DecodeAuthorization(const string& authorization,
    string& username, string& password)
{
	char decode[64];
	memset(decode, 0, 64);
	string strDecode;
	if (b64decode(authorization.length(), (char *)authorization.c_str(),
	    64, decode) == 0)
	{
		strDecode = decode;
		// Split the string into the username and password by finding the ':'
		unsigned int index = strDecode.find(':');
		if (index != string::npos)
		{
			username = strDecode.substr(0, index);
			password = strDecode.substr(index + 1, strDecode.length());
			return true;
		}
	}
	return false;
}

/**
 * Main workhorse that will take a given XML data and turn it into CommentData
 * and store them into the vector and map
 * NOTE:  This is a debug function to test if XML parsing is working correctly
 * @param filename Name of the XML file to test
 * @param isStory Bool determining if we're storying the comments in root or
 *        current
 * @return True if parsing went well.  False, otherwise.
 */
bool ThreadService::DebugParseXML(const char * file, bool isStory)
{
	// TODO: Constantly clear the current comments?
	ClearCommentData();
	
	// This is the XML doc that will hold and parse information
	TiXmlDocument doc(file);
	bool result = doc.LoadFile();
	
	if (result == true)
	{
		isLoadingStory = isStory;
		return ParseXML(doc);
	}
	return false;
}

/**
 * Helper function that will go through the provided XML and extract the
 * information into the class structures.
 * @param doc TiXmlDocument class that holds the XML information
 * @return True if the parsing was successful.  False otherwise.
 */
bool ThreadService::ParseXML(TiXmlDocument& doc)
{
	// Root element will contain the story ID, name, last page, and current page
	// TODO: Have a flag in the function that signals whether we'll overwrite
	// the last page as post threads last threads are not accurate
	TiXmlElement * root = doc.FirstChildElement();
	
	// If the root isn't even valid, this will fail
	if (root == NULL)
		return false;
	
	// Double check to make sure this is correct
	if ( strncmp(root->Value(), "comments", 8) == 0 )
	{
		int storyId, lastPage, page;
		if (root->QueryIntAttribute("story_id", &storyId) != TIXML_SUCCESS)
		    return false;
		storyPost.storyId = storyId;
		storyPost.storyName = root->Attribute("story_name");
		if (root->QueryIntAttribute("last_page", &lastPage) != TIXML_SUCCESS)
		    return false;
		storyPost.lastPage = lastPage;
		if (root->QueryIntAttribute("page", &page) != TIXML_SUCCESS)
		    return false;
		storyPost.page = page;
	}
	else
	{
		// Shouldn't reach here unless the XML was invalid
		return false;
	}
	
	// The next child elements should be comment, and we'll recurse through this
	TiXmlElement * child = NULL;	
	bool result;
	for (child = root->FirstChildElement(); child;
	     child = child->NextSiblingElement())
	{
		if ( strncmp(child->Value(), "comment", 7) == 0 )
		{
			result = ParseComments(0, child);
			// If there's any error at all, return immediately
			if (result == false)
				return false;
		}
	}
	return true;
}

/**
 * Recursive private function to extract all the comments out of the given post
 * @param depth How far down the reply chain we are
 * @param root Root thread comment to loop through
 * @return True if this was successful.  Will immediately return False on an
 *         error.
 */
bool ThreadService::ParseComments(unsigned int depth, TiXmlElement * root)
{
	// Sanity check to make sure the root isn't null
	if (root == NULL)
		return false;
	
	// The root will be a comment element, so parse out the post information
	// We parse string data later since those should always succeed, and if
	// they don't, they don't cause any problems.  Also, if they do fail,
	// that means the XML isn't correct, which shouldn't be the case.
	int replyCount, lastReplyId, id;
	if (root->QueryIntAttribute("reply_count", &replyCount) != TIXML_SUCCESS)
	    return false;
	
	// If the depth is anything but the root (0), then there won't be a
	// last_reply_id
	if (depth == 0)
		if (root->QueryIntAttribute("last_reply_id", &lastReplyId) != TIXML_SUCCESS)
		    return false;
	if (root->QueryIntAttribute("id", &id) != TIXML_SUCCESS)
	    return false;
	
	// Loop through the elements and extract data
	// NOTE:  Ignoring parcipants elements as I'm not using them
	TiXmlElement * child = NULL;
	for (child = root->FirstChildElement(); child; 
	     child = child->NextSiblingElement())
	{
		// Right after the comment element should be a body element which
		// contains the full post. After we get this information, we have
		// everything needed to store the comment data
		if ( strncmp(child->Value(), "body", 4) == 0 )
		{
			CommentData * cd = new CommentData();
			cd->replyCount = replyCount;
			cd->lastReplyId = lastReplyId;
			/*
			if (root->QueryStringAttribute("date", &cd->date) != TIXML_SUCCESS)
		        cd->date = "";
			if (root->QueryStringAttribute("category", &cd->category) != TIXML_SUCCESS)
				cd->category = "";
			if (root->QueryStringAttribute("author", &cd->author) != TIXML_SUCCESS)
				cd->author = "";
			if (root->QueryStringAttribute("preview", &cd->preview) != TIXML_SUCCESS)
				cd->preview = "";
		    */
			cd->id = id;
			cd->preview = root->Attribute("preview");
			cd->date = root->Attribute("date");
			cd->category = root->Attribute("category");
			cd->author = root->Attribute("author");
			
			// Currently using electroly's html parsing code to strip out HTML
			// information
			string body;
			if (child->GetText() != NULL)
				body = child->GetText();
			cd->body = html_entity_decode( (char *)body.c_str() );
			
			cd->depth = depth;
			
			if (isLoadingStory)
				rootComments.push_back(cd);
			else
				currentComments.push_back(cd);
		}
		else if ( strncmp(child->Value(), "comments", 8) == 0)
		{
			// Found another comments element, so recurse through these child
			// elements if it has any child comment elements
			TiXmlElement * commentChild = NULL;
			for (commentChild = child->FirstChildElement(); commentChild; 
				commentChild = commentChild->NextSiblingElement())
			{
				if ( strncmp(commentChild->Value(), "comment", 7) == 0 )
				{
					bool result = ParseComments(depth + 1, commentChild);
					// If there's any error at all, return immediately
					if (result == false)
						return false;
				}
			}
		}
	}
	return true;
}

/**
 * Completely clears the data structures
 */
void ThreadService::ClearAllCommentData()
{
	ClearRootCommentData();
	ClearCommentData();
}

/**
 * Clears the root comment data and removes the stories
 */
void ThreadService::ClearRootCommentData()
{
	for (unsigned int i = 0; i < rootComments.size(); i++)
	{
		delete rootComments[i];
		rootComments[i] = NULL;
	}
	rootComments.clear();
}

/**
 * Loops through the list of CommendData and erases them
 */
void ThreadService::ClearCommentData()
{
	for (unsigned int i = 0; i < currentComments.size(); i++)
	{
		delete currentComments[i];
		currentComments[i] = NULL;
	}
	currentComments.clear();
}

/**
 * Adds the XML tags required to send the post to the thread.
 * @param postData Data that will be modified with XML tags
 */
string ThreadService::MakePostToXML(const string& postData)
{
	return "<?xml version=\"1.0\" encoding=\"UTF-8\"?><body>" + 
	    postData + "</body>";
}

/**
 * Retrieves the story post information
 * @return const StoryPost& reference
 */
const StoryPost& ThreadService::GetStoryPost()
{
	return storyPost;
}

/**
 * Retrieves the comment information for the root comments
 * @return Array of CommentData pointers
 */
const vector<CommentData *>& ThreadService::GetRootComments()
{
	return rootComments;
}

/**
 * Retrieves the comments for the current thread
 * @return Array of CommentData pointers
 */
const vector<CommentData *>& ThreadService::GetCurrentComments()
{
	return currentComments;
}

/**
 * Retreives the current comment index from the current thread.
 * @return The CommandData at the index if valid. Null if it doesn't exist.
 */
const CommentData * ThreadService::GetCurrentComment()
{
    return GetCurrentComment( currentCommentIndex );
}

/**
 * Retreives a specific comment from the current thread.
 * @param index Comment index to get
 * @return The CommandData at the index if valid. Null if it doesn't exist.
 */
const CommentData * ThreadService::GetCurrentComment(unsigned int index)
{
    if (index < currentComments.size())
    {
        return currentComments[index];
    }
    return NULL;
}

/**
 * Retrieves the number of thread replies in the specified thread
 * @return unsigned int of the number of thread replies
 */
unsigned int ThreadService::GetThreadReplies(unsigned int id)
{
	// Attempt to find the id.  If it doesn't exist, then we return 0
	map<unsigned int, unsigned int>::iterator iter = threadReplies.find(id);
	if (iter != threadReplies.end())
	{
		return iter->second;
	}
	return 0;
}



unsigned int ThreadService::GetLastThreadReplies(unsigned int id)
{
	// Attempt to find the id.  If it doesn't exist, then we return 0
	map<unsigned int, unsigned int>::iterator iter = lastThreadReplies.find(id);
	if (iter != lastThreadReplies.end())
	{
		// TODO: Compare with the current thread replies to get the change
		// Actually, shouldn't this be with the root refreshing and a data in
		// the struct?
		return iter->second;
	}
	return 0;
}

/**
 * Returns the enum error from the HTTPClient.
 * @return Enum HTTPError from the client.
 */
const HTTPClient::Error ThreadService::GetRecentError()
{
    return client.GetRecentError();
}

/**
 * Returns the most recent error message.
 * #return Last string error message.
 */
const string ThreadService::GetRecentTextError()
{
	string msg;
	HTTPClient::Error error = client.GetRecentError();
	switch (error)
	{
		case HTTPClient::NoError:
			msg = "No error.";
			break;
		case HTTPClient::NoHost:
			msg = "No set host or API server.";
			break;
		case HTTPClient::ErrorCreatingSocket:
			msg = "Error in creating a socket!";
			break;
		case HTTPClient::UnableToConnectWithSocket:
			msg = "Unable to connect to the host with the socket!";
			break;
		case HTTPClient::UnableToSendToSocket:
			msg = "Unable to send data to the socket!";
			break;
		case HTTPClient::ReceivedInvalidData:
			msg = "Received invalid or no data from the host!";
			break;
		case HTTPClient::ConnectionTimedOut:
			msg = "Connection Timed Out.";
			break;
		default:
			msg = "Unknown error has occurred.";
			break;
	};
	return msg;
}
